Micron Document
🎖️GitЯра🎖️

Commit 1d221e2ddeeb0746f5965649640542975a28b416


Parents : 672b77b
Author : James Rich <2199651+jamesarich@users.noreply.github.com>
Signature : Signature validation error
Date : 2026-05-28T12:16:19-07:00
Committer : GitHub <noreply@github.com>
Date : 2026-05-28T19:16:19Z

fix(ble): stop BLE scan on background and downgrade connection priority (#5644)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Changes
Diff

diff --git a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
index e9b4a58cc3..a81a5a9b1f 100644
--- a/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
+++ b/core/ble/src/androidMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
@@ -79,3 +79,10 @@ internal actual fun Peripheral.requestHighConnectionPriority(): Boolean {
.onFailure { Logger.w(it) { "requestConnectionPriority(High) threw" } }
.getOrDefault(false)
}
+
+internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean {
+ val androidPeripheral = this as? AndroidPeripheral ?: return false
+ return runCatching { androidPeripheral.requestConnectionPriority(AndroidPeripheral.Priority.Balanced) }
+ .onFailure { Logger.w(it) { "requestConnectionPriority(Balanced) threw" } }
+ .getOrDefault(false)
+}

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt
index 0c3b839498..35cb3bee32 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/BleConnection.kt
@@ -78,6 +78,13 @@ interface BleConnection {
* Default implementation returns `false` for platforms that don't support it.
*/
fun requestHighConnectionPriority(): Boolean = false
+
+ /**
+ * Requests the platform to return to balanced BLE connection priority (default ~30–50 ms interval). Call after
+ * latency-sensitive operations (initial config drain, DFU) to reduce ongoing battery draw. Default implementation
+ * returns `false` for platforms that don't support it.
+ */
+ fun requestBalancedConnectionPriority(): Boolean = false
}
/** Represents a BLE service for commonMain. */

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleConnection.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleConnection.kt
index 5bb26c8a5a..6dc24c2c9a 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleConnection.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KableBleConnection.kt
@@ -245,6 +245,8 @@ class KableBleConnection(private val scope: CoroutineScope, private val loggingC
override fun requestHighConnectionPriority(): Boolean = peripheral?.requestHighConnectionPriority() == true
+ override fun requestBalancedConnectionPriority(): Boolean = peripheral?.requestBalancedConnectionPriority() == true
+
/** Ensures the previous peripheral's GATT resources are fully released. */
private suspend fun cleanUpPeripheral(tag: String) {
withContext(NonCancellable) { safeClosePeripheral(tag) }

diff --git a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
index a1f0baaefc..791e3c62b8 100644
--- a/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
+++ b/core/ble/src/commonMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
@@ -38,3 +38,9 @@ internal expect fun Peripheral.negotiatedMaxWriteLength(): Int?
* no-op returning `false`. Used by latency-sensitive flows such as DFU firmware streaming.
*/
internal expect fun Peripheral.requestHighConnectionPriority(): Boolean
+
+/**
+ * Requests balanced BLE connection priority (default ~30–50 ms interval) to reduce battery draw after latency-sensitive
+ * operations complete. On platforms without an equivalent API (JVM/iOS) this is a no-op.
+ */
+internal expect fun Peripheral.requestBalancedConnectionPriority(): Boolean

diff --git a/core/ble/src/iosMain/kotlin/org/meshtastic/core/ble/NoopStubs.kt b/core/ble/src/iosMain/kotlin/org/meshtastic/core/ble/NoopStubs.kt
index 85eb4fe7f4..756fb9cfe8 100644
--- a/core/ble/src/iosMain/kotlin/org/meshtastic/core/ble/NoopStubs.kt
+++ b/core/ble/src/iosMain/kotlin/org/meshtastic/core/ble/NoopStubs.kt
@@ -30,3 +30,5 @@ internal actual fun createPeripheral(address: String, builderAction: PeripheralB
internal actual fun Peripheral.negotiatedMaxWriteLength(): Int? = null
internal actual fun Peripheral.requestHighConnectionPriority(): Boolean = false
+
+internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean = false

diff --git a/core/ble/src/jvmMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt b/core/ble/src/jvmMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
index 462d7345da..16bd2fbc95 100644
--- a/core/ble/src/jvmMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
+++ b/core/ble/src/jvmMain/kotlin/org/meshtastic/core/ble/KablePlatformSetup.kt
@@ -33,4 +33,6 @@ internal actual fun Peripheral.negotiatedMaxWriteLength(): Int? = DEFAULT_JVM_MT
internal actual fun Peripheral.requestHighConnectionPriority(): Boolean = false
+internal actual fun Peripheral.requestBalancedConnectionPriority(): Boolean = false
+
private const val DEFAULT_JVM_MTU = 512

diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt
index 95512ecf47..37b90d526e 100644
--- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt
+++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/BleRadioTransport.kt
@@ -78,6 +78,16 @@ private val HEARTBEAT_DRAIN_DELAY = 200.milliseconds
private val SCAN_TIMEOUT = 5.seconds
private val GATT_CLEANUP_TIMEOUT = 5.seconds
+/**
+ * Delay after onConnect before downgrading BLE connection priority to Balanced.
+ *
+ * The initial config drain (fromRadio burst) typically completes within 2–5 seconds on most devices, but slower radios
+ * (ESP32 with large node databases, many channels, or dense position history) can take significantly longer. 30 seconds
+ * provides generous margin while still ensuring we don't sustain the 7.5 ms connection interval indefinitely, which
+ * significantly increases battery draw on both the phone and the radio.
+ */
+private val PRIORITY_DOWNGRADE_DELAY = 30.seconds
+
/**
* A [RadioTransport] implementation for BLE devices using the common BLE abstractions (which are powered by Kable).
*
@@ -361,15 +371,7 @@ class BleRadioTransport(
val maxLen = bleConnection.maximumWriteValueLength(BleWriteType.WITHOUT_RESPONSE)
Logger.i { "[$address] BLE Radio Session Ready. Max write length (WITHOUT_RESPONSE): $maxLen bytes" }
- // Ask the platform for a low-latency / high-throughput connection interval
- // (~7.5 ms on Android). The Meshtastic firmware happily accepts this and it
- // materially speeds up the initial config drain and any bulk fromRadio reads.
- if (bleConnection.requestHighConnectionPriority()) {
- Logger.d { "[$address] Requested high BLE connection priority" }
- // Wait for the connection parameter update to succeed before starting the heavy traffic
- // in onConnect(). Otherwise, the Android BLE stack may disconnect with GATT 147.
- delay(1.seconds)
- }
+ requestHighPriorityAndScheduleDowngrade()
this@BleRadioTransport.callback.onConnect()
}
@@ -398,6 +400,24 @@ class BleRadioTransport(
}
}
+ /**
+ * Requests high BLE connection priority for the initial config burst, then schedules a downgrade to balanced
+ * priority after [PRIORITY_DOWNGRADE_DELAY] to conserve battery.
+ */
+ private suspend fun CoroutineScope.requestHighPriorityAndScheduleDowngrade() {
+ if (bleConnection.requestHighConnectionPriority()) {
+ Logger.d { "[$address] Requested high BLE connection priority" }
+ // Wait for the connection parameter update before starting heavy traffic.
+ delay(1.seconds)
+ }
+ launch {
+ delay(PRIORITY_DOWNGRADE_DELAY)
+ if (bleConnection.requestBalancedConnectionPriority()) {
+ Logger.d { "[$address] Downgraded to balanced BLE connection priority" }
+ }
+ }
+ }
+
@Volatile private var radioService: MeshtasticRadioProfile? = null
// --- RadioTransport Implementation ---

diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
index b52d5013d3..d6e32c1513 100644
--- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
+++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
@@ -38,8 +38,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -48,6 +46,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@@ -136,17 +135,17 @@ fun ConnectionsScreen(
onDenied = { scanModel.stopNetworkScan() },
)
- // Auto-start BLE scan on screen entry when the user has previously opted in. Stop on screen exit to save battery.
- // We use `Unit` as the key so the effect is stable across recompositions — the toggle button manages scan
- // start/stop directly and must not be interrupted by this effect re-firing when `bleAutoScan` changes.
- // A LaunchedEffect watches bleAutoScan separately to handle the DataStore delivering `true` after the initial
- // `false` (disk read latency), without triggering a dispose/restart cycle that kills an in-progress scan.
- LaunchedEffect(bleAutoScan) { if (bleAutoScan && !scanModel.isBleScanning.value) scanModel.startBleScan() }
- DisposableEffect(Unit) { onDispose { scanModel.stopBleScan() } }
+ // Auto-start BLE scan when the screen is visible (lifecycle ≥ STARTED) and the user has previously opted in.
+ // LifecycleStartEffect stops scanning on ON_STOP (app backgrounded) and restarts on ON_START — preventing
+ // continuous background BLE radio usage that drains the battery.
+ LifecycleStartEffect(bleAutoScan) {
+ if (bleAutoScan && !scanModel.isBleScanning.value) scanModel.startBleScan()
+ onStopOrDispose { scanModel.stopBleScan() }
+ }
- DisposableEffect(networkAutoScan, localNetworkPermissionGranted) {
+ LifecycleStartEffect(networkAutoScan, localNetworkPermissionGranted) {
if (networkAutoScan && localNetworkPermissionGranted) scanModel.startNetworkScan()
- onDispose { scanModel.stopNetworkScan() }
+ onStopOrDispose { scanModel.stopNetworkScan() }
}
/* Animate waiting for the configurations */

Served by rngit 1.5.0 - Generated in 0.33s